package org.interaction3d.assembly.sax;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Locale;
import java.util.Scanner;

import java.util.logging.ConsoleHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import org.interaction3d.assembly.CoordinateFrame;
import static javax.xml.XMLConstants.*;
import org.interaction3d.assembly.Model;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;



public class SAXModelReader
{
    // <editor-fold defaultstate="collapsed" desc=" ModelHandler ">

    private static class ModelHandler extends DefaultHandler
    {
        enum Modifer
        {
            MORPH
            {
                void blend(Model model, String name)//, int blend)
                {
                    model.morphDeformer(name);//, blend);
                }
            },
            SKIN
            {
                void blend(Model model, String name)//, int blend)
                {
                    model.skinDeformer(name);//, blend);
                }
            };

            abstract void blend(Model model, String name);//, int blend);
        }

        private final Model model;
        private final double[] coordinate = new double[3];

        private CoordinateFrame coodinateFrame;
        private String axis;
        private Modifer modifier;


        private Locator locator;
        private AttributeParser ap;
        private boolean readCData = false;
        private StringBuilder cdata = new StringBuilder();

        public ModelHandler(Model model)
        {
            if(model == null)
            {
                throw new NullPointerException();
            }

            this.model = model;
        }

        @Override
        public void characters(char[] ch, int start, int length) throws SAXException
        {
            if (readCData)
                this.cdata.append(ch, start, length);
        }

        @Override
        public void setDocumentLocator(Locator locator)
        {
            this.ap = new AttributeParser(locator);
            this.locator = locator;
        }

        @Override
        public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
        {
            if (qName.equals("Model"))
            {                
            }
            else if (qName.equals("Skeleton"))
            {
                int joints = this.ap.parseInt(attributes, "joints");
                model.joints(joints);
            }
            else if (qName.equals("Joint"))
            {
                String name = this.ap.parseString(attributes, "name");
                coodinateFrame = model.joint(name);                                
            }
            else if (qName.equals("Origin"))
            {
                this.cdata.setLength(0);
                this.readCData = true;
            }
            else if (qName.equals("Axis"))
            {
                axis = this.ap.parseString(attributes, "name");

                this.cdata.setLength(0);
                this.readCData = true;
            }
            else if (qName.equals("Geometry"))
            {
                int meshes = this.ap.parseInt(attributes, "parts");
                model.meshes(meshes);
            }
            else if (qName.equals("Mesh"))
            {
                String name = this.ap.parseString(attributes, "name");
                int space = -1;//this.ap.parseInt(attributes, "space", -1);
                
                model.mesh(name, space);
            }
            else if (qName.equals("Morph"))
            {                
                int count = this.ap.parseInt(attributes, "count");

                model.morphDeformers(count);
                modifier = Modifer.MORPH;
            }
            else if (qName.equals("Skin"))
            {
                int count = this.ap.parseInt(attributes, "count");

                model.skinDeformers(count);
                modifier = Modifer.SKIN;
            }
            else if (qName.equals("Modifier"))
            {
                this.cdata.setLength(0);
                this.readCData = true;
            }
        }

        @Override
        public void endElement(String uri, String localName, String qName) throws SAXException
        {
            this.readCData = false;

            if (qName.equals("Origin"))
            {
                parseCoordinate(cdata, coordinate);

                coodinateFrame.origin(coordinate[0], coordinate[1], coordinate[2]);

                cdata.setLength(0);
                this.readCData = true;
            }
            else if (qName.equals("Axis"))
            {
                parseCoordinate(cdata, coordinate);

                if(axis.equalsIgnoreCase("X"))
                {
                    coodinateFrame.xAxis(coordinate[0], coordinate[1], coordinate[2]);
                }
                else if(axis.equalsIgnoreCase("Y"))
                {
                    coodinateFrame.yAxis(coordinate[0], coordinate[1], coordinate[2]);
                }
                else if(axis.equalsIgnoreCase("Z"))
                {
                    coodinateFrame.zAxis(coordinate[0], coordinate[1], coordinate[2]);
                }
                else
                {
                    throw new RuntimeException("Olny X,Y,Z are supported axis names");
                }
                
                cdata.setLength(0);
                this.readCData = true;
            }
            else if (qName.equals("Modifier"))
            {
                String name = cdata.toString().trim();
                modifier.blend(model, name);

                cdata.setLength(0);
                this.readCData = true;
            }
        }

        @Override
        public void error(SAXParseException e)
        {
            String msg = "Error at " + locator.getLineNumber()  + ":" + locator.getColumnNumber();
            handle(e, msg);
        }

        @Override
        public void fatalError(SAXParseException e)
        {
            String msg = "Fatal error at " + locator.getLineNumber()  + ":" + locator.getColumnNumber();
            handle(e, msg);
        }

        @Override
        public void warning(SAXParseException e)
        {
            String msg = "Warning at " + locator.getLineNumber()  + ":" + locator.getColumnNumber();
            handle(e, msg);
        }
    }

//    static private void parseVertices(CharSequence coordinates, int count, int elemets, int[] selection, Mesh mesh)
//    {
//        final Scanner scanner = new Scanner(coordinates.toString()).useLocale(Locale.ENGLISH);
//
//        int[] allElems = new int[elemets];
//        int[] selectedElems = new int[selection.length];
//
//        for(int numMatches=0; numMatches<count; numMatches++)
//        {
//            for(int i=0; i<allElems.length; i++)
//            {
//                allElems[i] = scanner.nextInt();
//            }
//
//            for(int i=0; i<selection.length; i++)
//            {
//                selectedElems[i] = (selection[i] < 0)
//                                 ? numMatches
//                                 : allElems[selection[i]];
//            }
//
//            mesh.vertex(selectedElems);
//        }
//    }


    static private void parseCoordinate(CharSequence coordinates, double[] coord)
    {
        final Scanner scanner = new Scanner(coordinates.toString()).useLocale(Locale.ENGLISH);

        for(int i=0; i<coord.length; i++)
        {
            coord[i] = scanner.nextDouble();
        }

        if(scanner.hasNext())
        {
            throw new RuntimeException("Coodinate Dimension mismatch!");
        }
    }
//
//    static private void parseCoordinateRefs(CharSequence coordinates, int count, Mesh mesh)
//    {
//        final Scanner scanner = new Scanner(coordinates.toString()).useLocale(Locale.ENGLISH);
//
//        for(int numMatches=0; numMatches<count; numMatches++)
//        {
//            mesh.coordinate(scanner.nextInt());
//        }
//    }
//
//    static private void parseTriangles(CharSequence coordinates, int count, Mesh mesh)
//    {
//        final Scanner scanner = new Scanner(coordinates.toString()).useLocale(Locale.ENGLISH);
//
//        int[] triangle = new int[3];
//        for(int numMatches=0; numMatches<count; numMatches++)
//        {
//            for(int i=0; i<triangle.length; i++)
//            {
//                triangle[i] = scanner.nextInt();
//            }
//            mesh.primtive(triangle);
//        }
//    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Variables ">

    private final SAXParser parser;

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Constructors ">

    private SAXModelReader(SAXParserFactory factory) throws ParserConfigurationException, SAXException
    {
        URL schemaURL = getClass().getResource("/org/interaction3d/assembly/Model.xsd");
        Schema schema = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI).newSchema(schemaURL);
        factory.setSchema(schema);
        factory.setNamespaceAware(true);//impoartant for xsd
        factory.setValidating(false);//only for dtd
        parser = factory.newSAXParser();
    }

    public SAXModelReader() throws ParserConfigurationException, SAXException, IllegalArgumentException, IOException
    {
        this(SAXParserFactory.newInstance());
    }

    // </editor-fold>

    // <editor-fold defaultstate="collapsed" desc=" Methods ">

    public Model.Assembly assembly(final String uri)
    {
        return new Model.Assembly()
        {
            public void assemble(Model model)
            {
                try
                {
                    parser.parse(uri, new ModelHandler(model));
                }
                catch (SAXException ex)
                {
                    handle(ex);
                }
                catch (IOException ex)
                {
                    handle(ex);
                }
            }
        };
    }

    public Model.Assembly assembly(final InputSource is)
    {
        return new Model.Assembly()
        {
            public void assemble(Model model)
            {
                try
                {
                    parser.parse(is, new ModelHandler(model));
                }
                catch (SAXException ex)
                {
                    handle(ex);
                }
                catch (IOException ex)
                {
                    handle(ex);
                }
            }
        };
    }

    public Model.Assembly assembly(final InputStream in)
    {
        return new Model.Assembly()
        {
            public void assemble(Model model)
            {
                try
                {
                    parser.parse(in, new ModelHandler(model));
                }
                catch (SAXException ex)
                {
                    handle(ex);
                }
                catch (IOException ex)
                {
                    handle(ex);
                }
            }
        };
    }

    public Model.Assembly assembly(final File f)
    {
        return new Model.Assembly()
        {
            public void assemble(Model model)
            {
                try
                {
                    parser.parse(f, new ModelHandler(model));
                }
                catch (SAXException ex)
                {
                    handle(ex);
                }
                catch (IOException ex)
                {
                    handle(ex);
                }
            }
        };
    }

    // </editor-fold>

    static private void handle(Exception ex)
    {
        handle(ex, null);
    }

    static private void handle(Exception ex, String msg)
    {
        Logger.getLogger(SAXModelReader.class.getName()).log(Level.SEVERE, msg, ex);
        throw new RuntimeException(ex);
    }

    static
    {
        Logger logger = Logger.getLogger(SAXModelReader.class.getName());
        logger.setLevel(Level.ALL);
        logger.addHandler(new ConsoleHandler());
    }
}